package de.programmerspain.rv3sf.api;

import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.Validate;

/**
 * A gamma table implementation used to limit the min and max value
 * of each RGB color channel. This is can be used to boost / decrease
 * a certain color channel or to limit the overall brightness.
 * 
 * @author Markus Lang (m@rkus-lang.de) | http://programmers-pain.de/ | https://code.google.com/p/rainbowduino-v3-streaming-firmware/
 */
public class GammaTable {
	private static final short NUMBER_OF_COLORS = 3;
	private static final short NUMBER_OF_VALUES = 256;
	private static final short MIN_VALUE = 0;
	private static final short MAX_VALUE = 255;
	
	private short[][] gammaValues;

	/**
	 * empty constructor if you want to create an bypass instance
	 * of this class that shouldn't change any colors values at all
	 */
	public GammaTable() {
	}
	
	/**
	 * this constructor can be used to limit one or many color channels
	 * with the provided <code>minValue</code> and <code>maxValue</code>
	 * values. Every non-specified color channel will be initialized with
	 * the default min / max values. If you provide an emtpy <code>colors</code>
	 * colorEnum array than the provided min / max values will be applied
	 * to all three color channels.
	 * 
	 * @param minValue the minimum value of a color channel
	 * @param maxValue the maximum value of a color channel
	 * @param colors an optional <code>ColorEnum</code> array
	 */
	public GammaTable(short minValue, short maxValue, ColorEnum... colors) {
		// validate given variables
		Validate.notNull(minValue);
		Validate.notNull(maxValue);
		Validate.validState(minValue >= 0 && minValue < NUMBER_OF_VALUES, "minValue: " + minValue + " isn't in the allowed range of 0 -> 255!");
		Validate.validState(maxValue >= 0 && maxValue < NUMBER_OF_VALUES, "maxValue: " + maxValue + " isn't in the allowed range of 0 -> 255!");
		Validate.validState(minValue < maxValue, "minValue: " + minValue + " has to be smaller than maxValue: " + maxValue + "!");
		// initialize gamma values array
		this.gammaValues = new short[NUMBER_OF_COLORS][NUMBER_OF_VALUES];
		// calculate values in case the colors array isn't empty
		if (colors.length > 0) {
			// calculate values for the given colors
			for (ColorEnum color : colors) {
				this.initializeColor(color, minValue, maxValue);
			}
			// use default min/max values for the non-specified colors
			for (ColorEnum color : ColorEnum.values()) {
				if (ArrayUtils.contains(colors, color)) {
					continue;
				}
				this.initializeColor(color, MIN_VALUE, MAX_VALUE);
			}
		} else {
			// initialize all colors with the given min/max values
			for (ColorEnum color : ColorEnum.values()) {
				this.initializeColor(color, minValue, maxValue);
			}
		}
	}
	
	/**
	 * constructs a new instance that will be initialized using the
	 * specified limit values of the <code>gammaTableConfiguration</code> instance.
	 * 
	 * @param gammaTableConfiguration a <code>GammaTableConfiguration</code> instance
	 */
	public GammaTable(GammaTableConfiguration gammaTableConfiguration) {
		// validate given variables
		Validate.notNull(gammaTableConfiguration);
		// initialize gamma values array
		this.gammaValues = new short[NUMBER_OF_COLORS][NUMBER_OF_VALUES];
		// calculate color values using the given color value min/max limits
		for (ColorEnum color : ColorEnum.values()) {
			this.initializeColor(
					color,
					gammaTableConfiguration.getMin(color),
					gammaTableConfiguration.getMax(color)
			);
		}
	}

	/**
	 * translates the given color value into a new color value based
	 * on the color channel limits of this <code>GammaTable</code> instance.
	 * 
	 * @param color the color channel
	 * @param value the original color value
	 * @return the translated color value
	 */
	public short getValue(ColorEnum color, short value) {
		if (this.gammaValues == null) {
			return value;
		}
		return this.gammaValues[color.ordinal()][value];
	}
		
	/**
	 * returns a string representation of the internal translation table
	 * per color channel.
	 * 
	 * @see java.lang.Object#toString()
	 */
	@Override
	public String toString() {
		if (this.gammaValues == null) {
			return "gammaTable instance isn't initialized. The given values will be returned without any modifications.";
		}
		StringBuffer stringBuffer = new StringBuffer();
		for (short color = 0; color < NUMBER_OF_COLORS; color++) {
			stringBuffer.append("gamma table for color: " + ColorEnum.values()[color] + "\n");
			for (short i = 0; i < 16; i++) {
				for (short j = 0; j < 16; j++) {
					stringBuffer.append(String.format("%3s", String.valueOf(this.gammaValues[color][(j + (i * 16))])));
					stringBuffer.append(" ");
				}
				stringBuffer.append("\n");
			}
		}
		return stringBuffer.toString();
	}
	
	// internal helper method that calculates the translation values in a linear scale
	private void initializeColor(ColorEnum color, short minValue, short maxValue) {
		if (minValue == MIN_VALUE && maxValue == MAX_VALUE) {
			for (short value = 0; value < NUMBER_OF_VALUES; value++) {
				this.gammaValues[color.ordinal()][value] = value;
			}
		} else {
			float factor = (float) MAX_VALUE / (maxValue - minValue);
			for (short value = 0; value < NUMBER_OF_VALUES; value++) {
				this.gammaValues[color.ordinal()][value] = (short) Math.round(((value / factor) + minValue));
			}
		}
	}
}
